//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using WF = System.Windows.Forms;

namespace Microsoft.Research.DataLayer
{
    /// <summary>
    /// Manages connections to data storages supported by eResearch platform.
    /// </summary>
    public abstract class ConnectionManagerBase
    {
        /// <summary>
        /// Handler for OnBeginConnection event
        /// </summary>
        /// <param name="type"></param>
        /// <param name="display"></param>
        /// <param name="cancel"></param>
        public delegate void BeginConnectionHandler(string type, string display, ref bool cancel);

        /// <summary>
        /// Event to be fired when a new connection is about to be established
        /// </summary>
        public event BeginConnectionHandler OnBeginConnection;

        protected ConfigScope scope = ConfigScope.Normal;
        protected string alternateStorage = null;
        private datasource.ConfigSources settings;
        private Dictionary<string, ProviderPkgInfo> providers;

        /// <summary>
        /// Determines behavior when dealing with user cancellations
        /// </summary>
        public enum CancelBehavior
        {
            /// <summary>
            /// Returns null to the caller
            /// </summary>
            SilentlyReturnNull,

            /// <summary>
            /// Throws an exception on cancellation
            /// </summary>
            ThrowException,
        }

        /// <summary>
        /// Connection Manager GUI behavior
        /// </summary>
        public enum ConnectionUI
        {
            /// <summary>
            /// Always shows GUI, even if a default connection exists
            /// </summary>
            AlwaysShowUI,

            /// <summary>
            /// Prompts GUI only if no default connection exists
            /// </summary>
            PromptIfNeeded,

            /// <summary>
            /// Never shows GUI, even if a default connection does not exist
            /// </summary>
            NeverShowUI,
        }

        public enum ConfigScope
        {
            Normal,
            Template,
        }

        /// <summary>
        /// Determines the style of the connection dialog displayed
        /// </summary>
        public enum UIStyleType
        {
            /// <summary>
            /// Regular "full" connection with details/management
            /// </summary>
            Normal,

            /// <summary>
            /// Simple, connect only GUI with list of connections
            /// </summary>
            Lite,
        }

        public List<ProviderPkgInfo> Providers
        {
            get { return new List<ProviderPkgInfo>(providers.Values); }
        }

        public static string AlternateProviderLocation
        {
            get { return ProviderHelper.AlternateProviderLocation; }
            set { ProviderHelper.AlternateProviderLocation = value; }
        }

        public ConfigScope Scope
        {
            get { return this.scope; }
            set { this.scope = value; }
        }

        protected bool HasAlternateStorage
        {
            get
            {
                return !string.IsNullOrEmpty(alternateStorage);
            }
        }

        /// <summary>
        /// Returns true if the system has a default/preferred connection
        /// </summary>
        public bool HasDefaultConnection
        {
            get
            {
                return (settings != null && settings.DefaultProvider != null);
            }
        }

        internal void FireOnBeginConnection(string type, string msg, ref bool cancel)
        {
            if (OnBeginConnection != null)
                OnBeginConnection(type, msg, ref cancel);

            if (cancel && cancelBehavior == CancelBehavior.ThrowException)
            {
                throw new UserCancelledConnectionPicker();
            }
        }

        private Connection DefaultConnectionCache = null;
        private Connection CurrentConnection = null;

        /// <summary>
        /// Returns a default (saved) connection
        /// </summary>
        protected Connection GetDefaultConnection()
        {
            if (DefaultConnectionCache == null)
            {
                Connection ret = null;

                // Check if we have a default connection
                datasource.Reference defProv = settings.DefaultProvider;
                if (defProv != null &&
                    !string.IsNullOrEmpty(defProv.connection_id) &&
                    !string.IsNullOrEmpty(defProv.provider))
                {
                    // Do we have the provider for it?
                    ProviderPkgInfo provider = ProviderHelper.GetProviderByName(defProv.provider);

                    // Load settings into provider
                    datasource.Provider provSettings = settings.GetByProvider(provider.ProviderKey);
                    provider.FromXml(provSettings);
                    provider.Dialog.LoadSettings();

                    // Ask the provider to build the connection
                    ret = provider.GetConnectionByID(new Guid(defProv.connection_id));
                }

                if (ret != null)
                    ret.Open();

                DefaultConnectionCache = ret;
            }

            return DefaultConnectionCache;
        }

        private CancelBehavior cancelBehavior = CancelBehavior.ThrowException;
        private UIStyleType uiStyle = UIStyleType.Normal;

        /// <summary>
        /// Get/set the current GUI style
        /// </summary>
        public UIStyleType UIStyle
        {
            get { return uiStyle; }
            set { uiStyle = value; }
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="cancelBehavior"></param>
        protected ConnectionManagerBase(CancelBehavior cancelBehavior)
        {
            this.cancelBehavior = cancelBehavior;
            Init0();
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="cancelBehavior"></param>
        /// <param name="alternateStorage"></param>
        protected ConnectionManagerBase(CancelBehavior cancelBehavior, string alternateStorage)
        {
            this.cancelBehavior = cancelBehavior;
            this.alternateStorage = alternateStorage;
            Init0();
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="cancelBehavior"></param>
        /// <param name="alternateStorage"></param>
        protected ConnectionManagerBase(CancelBehavior cancelBehavior, ConfigScope scope)
        {
            this.cancelBehavior = cancelBehavior;
            this.scope = scope;
            Init0();
        }

        private void Init0()
        {
            try
            {
                Init();
            }
            catch (InvalidProviderConfiguration)
            {
                if (cancelBehavior != CancelBehavior.SilentlyReturnNull)
                    throw;
            }
            catch (ConnectionStoreFileNotFound)
            {
                if (cancelBehavior != CancelBehavior.SilentlyReturnNull)
                    throw;
            }
        }

        /// <summary>
        /// Selects a connection to use
        /// </summary>
        /// <returns>A valid connection to the storage</returns>
        public Connection PickConnection(ConnectionUI behavior)
        {
            return PickConnection(behavior, null, false);
        }

        /// <summary>
        /// Selects a connection to use
        /// </summary>
        /// <returns>A valid connection to the storage</returns>
        public Connection PickConnection(ConnectionUI behavior, WF.Control parent, bool alwaysOnTop)
        {
            return ManageOrPickConnection(behavior, parent, alwaysOnTop, true);
        }

        public void ManageConnections(WF.Control parent, bool alwaysOnTop)
        {
            ManageOrPickConnection(ConnectionUI.AlwaysShowUI, parent, alwaysOnTop, false);
        }

        public Connection OpenConnection(IConnMgrInfoParams conn)
        {
            ProviderPkgInfo prov = GetProviderForConnection(conn);

            // We found a provider that can handle the connection
            return prov.OpenConnection(conn);
        }

        public bool TestConnection(IConnMgrInfoParams conn, out BackendStorageException error)
        {
            ProviderPkgInfo prov = GetProviderForConnection(conn);

            // We found a provider that can handle the connection
            // Now let's ask him to test it
            return prov.TestConnection(conn, out error);
        }

        private IConnectionManagerDialog CreateConnectionDialog(bool allowConnect)
        {
            switch (uiStyle)
            {
                case UIStyleType.Normal:
                    return new ConnectionManagerDialog(this, ref settings, allowConnect);
                case UIStyleType.Lite:
                    {
                        if (!allowConnect)
                            throw new CannotManageWithLiteDialogException();

                        return new ConnectionManagerDialogLite(this, ref settings, allowConnect);
                    }
                default:
                    throw new NotImplementedException();
            }
        }

        /// <summary>
        /// Selects a connection to use
        /// </summary>
        /// <returns>A valid connection to the storage</returns>
        private Connection ManageOrPickConnection(ConnectionUI behavior, WF.Control parent, bool alwaysOnTop, bool allowConnect)
        {
            if (!HasDefaultConnection && behavior == ConnectionUI.NeverShowUI)
            {
                throw new ApplicationException("Default not specified and asked to be silent. Cannot proceed");
            }

            if (settings == null)
                return null;

            WF.MethodInvoker method = new WF.MethodInvoker(delegate
            {
                IConnectionManagerDialog connDlg;
                connDlg = CreateConnectionDialog(allowConnect);
                connDlg.TopMost = alwaysOnTop;
                connDlg.OnSave += new ConnectionManagerDialog.SaveHandler(SaveSettings);
                connDlg.ShowDialog(parent);
                CurrentConnection = connDlg.Connection;
            });

            if (behavior == ConnectionUI.AlwaysShowUI ||
                (!HasDefaultConnection && CurrentConnection == null))
            {
                if (parent != null)
                {
                    // In the main message loop thread
                    parent.Invoke(method);

                    if (CurrentConnection != null)
                        CurrentConnection.Open();

                    return CurrentConnection;
                }
                else
                {
                    // Run without threads
                    method.Invoke();

                    if (CurrentConnection != null)
                        CurrentConnection.Open();

                    return CurrentConnection;
                }
            }
            else
            {
                if (CurrentConnection == null)
                    CurrentConnection = GetDefaultConnection();

                return CurrentConnection;
            }
        }

        private void Init()
        {
            // Initialize the providers to read the configuration
            providers = ProviderHelper.GetProviders();

            try
            {
                // Load config sources
                using (StreamReader sr = new StreamReader(ConfigFile))
                {
                    XmlSerializer xmlSrc = new XmlSerializer(typeof(datasource.ConfigSources));

                    // Load entries
                    try
                    {
                        settings = (datasource.ConfigSources)xmlSrc.Deserialize(sr);

                        if (settings.version == null || new Version(settings.version) != datasource.ConfigSources.CurrentVersion)
                        {
                            throw new InvalidProviderConfiguration();
                        }
                    }
                    catch (InvalidOperationException)
                    {
                        // Bad or empty XML. Revert to empty list.
                        throw new FileNotFoundException();
                    }
                }
            }
            catch (FileNotFoundException)
            {
                throw new ConnectionStoreFileNotFound(ConfigFile);
            }

            // Test default connection health
            if (settings.DefaultProvider != null)
            {
                if (!providers.ContainsKey(settings.DefaultProvider.provider))
                    throw new ProviderMissingException(settings.DefaultProvider.provider);
            }

            // Test last used settings health
            if (settings.LastUsedProvider != null)
            {
                if (!providers.ContainsKey(settings.LastUsedProvider.provider))
                    throw new ProviderMissingException(settings.LastUsedProvider.provider);
            }

            // Now iterate through all the configuration. 
            foreach (datasource.Provider prov in settings.DataSources)
            {
                // We need to make sure no configuration was left behind
                if (!providers.ContainsKey(prov.name))
                    throw new ProviderMissingException(prov.name);

                ProviderPkgInfo provHandler = providers[prov.name];
                provHandler.FromXml(prov);
            }
        }

        /// <summary>
        /// Creates a new store with no connections on it
        /// </summary>
        public void CreateEmptyStore()
        {
            // Check if the folder exists
            string storeFile = ConfigFileName;
            string fileDir = Path.GetDirectoryName(storeFile);
            if (!Directory.Exists(fileDir))
            {
                // Try to create the folder
                Directory.CreateDirectory(fileDir);
            }

            File.Create(ConfigFileName).Close();

            ClearConnections();
        }

        /// <summary>
        /// Deletes the store
        /// </summary>
        public void DeleteStore()
        {
            if (File.Exists(ConfigFileName))
            {
                ClearConnections();
                File.Delete(ConfigFileName);
            }
        }

        public void MergeWithTemplate()
        {
            if (scope == ConfigScope.Template)
            {
                throw new InvalidTargetForMergeException();
            }

            // Clear provider cache
            ProviderHelper.ReloadProviders();

            ConnectionManagerBase template = this.CreateNewInstance(CancelBehavior.ThrowException, ConfigScope.Template);

            // #1) Load all template connections in list
            Dictionary<Guid, IConnMgrInfoParams> templateConnections = new Dictionary<Guid, IConnMgrInfoParams>();
            foreach (ProviderPkgInfo provider in template.Providers)
            {
                foreach (IConnMgrInfoParams conn in provider.Presets)
                {
                    templateConnections.Add(conn.ID, conn);
                }
            }

            // #2) Remove all connections on target that matches
            foreach (ProviderPkgInfo provider in this.Providers)
            {
                List<IConnMgrInfoParams> connList = new List<IConnMgrInfoParams>(provider.Presets);
                foreach (IConnMgrInfoParams conn in connList)
                {
                    if (templateConnections.ContainsKey(conn.ID))
                    {
                        this.RemoveConnection(conn);
                    }
                }
            }

            // #3) Copy all template connections to user
            foreach (IConnMgrInfoParams conn in templateConnections.Values)
            {
                this.AddConnection(conn);
            }
        }

        /// <summary>
        /// Check whether preset exists in ConfigSource file or not.
        /// </summary>
        /// <param name="presetName">Preset Name</param>
        /// <returns>True/False</returns>
        public bool IsPresetNameExists(string presetName)
        {
            bool result = false;

            if (settings.DataSources != null)
            {
                foreach (datasource.Provider provSettings in settings.DataSources)
                {
                    if (provSettings.DataSource != null)
                    {
                        foreach (datasource.DataSource datasource in provSettings.DataSource)
                        {
                            if (String.Compare(datasource.name, presetName, StringComparison.InvariantCultureIgnoreCase) == 0)
                            {
                                result = true;
                                break;
                            }
                        }
                    }

                    if (result) break;
                }
            }

            return result;
        }

        /// <summary>
        /// Used to create a new instance of the connection manager from a running connection manager
        /// </summary>
        /// <param name="cancelBehavior"></param>
        /// <param name="scope"></param>
        /// <returns></returns>
        protected abstract ConnectionManagerBase CreateNewInstance(CancelBehavior cancelBehavior, ConfigScope scope);

        /// <summary>
        /// Returns the configuration file name
        /// </summary>
        protected abstract string ConfigFile { get; }

        /// <summary>
        /// Return the default configuration file name
        /// </summary>
        protected abstract string DefaultConfigFile { get; }

        /// <summary>
        /// Return the default configuration file name
        /// </summary>
        protected abstract string ConfigFileName { get; }

        /// <summary>
        /// Returns the name of the config file being used
        /// </summary>
        public string ConfigDisplayName
        {
            get
            {
                switch (scope)
                {
                    case ConfigScope.Normal:
                return Path.GetFileName(ConfigFileName).ToUpperInvariant();
                    case ConfigScope.Template:
                        return string.Format("Template/{0}", Path.GetFileName(ConfigFileName)).ToUpperInvariant();
                    default:
                        throw new NotImplementedException("Unknown scope");
                }
            }
        }

        private void SaveSettings(ConnectionManagerDialog.ConnSaveAction action)
        {
            string fileName;

            fileName = ConfigFile;

            using (StreamWriter sw = new StreamWriter(fileName))
            {
                XmlSerializer xmlSrc = new XmlSerializer(typeof(datasource.ConfigSources));

                if (settings == null)
                    settings = new datasource.ConfigSources();

                settings.version = datasource.ConfigSources.CurrentVersion.ToString();

                // Update the <Provider> settings
                List<datasource.Provider> provSources = new List<datasource.Provider>();
                foreach (ProviderPkgInfo prov in providers.Values)
                {
                    datasource.Provider provConfig = prov.ToXml();
                    if (provConfig != null)
                        provSources.Add(provConfig);
                }
                settings.DataSources = provSources.ToArray();

                xmlSrc.Serialize(sw, settings);
            }
        }

        /// <summary>
        /// Gets a provider capable of handling the connection
        /// </summary>
        /// <param name="conn"></param>
        /// <returns></returns>
        private ProviderPkgInfo GetProviderForConnection(IConnMgrInfoParams conn)
        {
            foreach (ProviderPkgInfo prov in Providers)
            {
                if (prov.HandlesConnection(conn))
                    return prov;
            }

            // No provider is able to handle this connection
            throw new ProviderMissingException2(conn);
        }

        public IConnMgrInfoParams GetConnectionByID(Guid id)
        {
            foreach (ProviderPkgInfo prov in providers.Values)
            {
                foreach (IConnMgrInfoParams conn in prov.Presets)
                {
                    if (conn.ID.Equals(id))
                    {
                        return conn;
                    }
                }
            }

            return null;
        }

        public IConnMgrInfoParams DefaultConnection
        {
            get
            {
                if (settings == null || settings.DefaultProvider == null)
                    return null;

                IConnMgrInfoParams conn = GetConnectionByID(new Guid(settings.DefaultProvider.connection_id));
                return conn;
            }

            set
            {
                if (value == null)
                {
                    // Caller wants to clear the default connection
                    settings.DefaultProvider = null;
                }
                else
                {
                    IConnMgrInfoParams savedConn = GetConnectionByID(value.ID);

                    if (savedConn == null)
                        throw new ConnectionNotFoundException();

                    ProviderPkgInfo prov = GetProviderForConnection(savedConn);

                    // The connection indeed belong to us. Make it default
                    settings.DefaultProvider = new datasource.Reference();
                    settings.DefaultProvider.provider = prov.ProviderKey;
                    settings.DefaultProvider.connection_id = savedConn.ID.ToString();
                }

                SaveSettings(ConnectionManagerDialog.ConnSaveAction.SaveDefault);
            }
        }

        /// <summary>
        /// Add a new connection to the store
        /// </summary>
        /// <param name="newConn">Connection parameters to be added</param>
        public void AddConnection(IConnMgrInfoParams newConn)
        {
            ProviderPkgInfo prov = GetProviderForConnection(newConn);

            // We found a provider that can handle the connection
            prov.Presets.Add(newConn);
            SaveSettings(ConnectionManagerDialog.ConnSaveAction.SavePresets);
            return;
        }

        /// <summary>
        /// Remove connection from the store
        /// </summary>
        /// <param name="newConn">Connection parameters to be removed</param>
        public void RemoveConnection(IConnMgrInfoParams delConn)
        {
            ProviderPkgInfo prov = GetProviderForConnection(delConn);

            // We found a provider that can handle the connection
            prov.Presets.Remove(delConn);
            SaveSettings(ConnectionManagerDialog.ConnSaveAction.SavePresets);
            return;
        }

        /// <summary>
        /// Remove all connections from the store
        /// </summary>
        public void ClearConnections()
        {
            foreach (ProviderPkgInfo prov in Providers)
            {
                if (prov.Presets != null && prov.Presets.Count > 0)
                    prov.Presets.Clear();
            }

            SaveSettings(ConnectionManagerDialog.ConnSaveAction.SavePresets);
        }
    }
}

